; 本示例演示读取软盘指定个扇区的操作（从第二个扇区开始读取）
; 假定，本程序位于引导扇区（512字节）
; 读取的软盘为第1个软盘，读取大小为1000个扇区，读取到内存0x10000起始的地址

LastSectorNoToRead      equ     1000            ; 待读取的最后一个扇区号，假设最大不超过1000个扇区，也就是1000 * 512字节 = 500K的大小，注意不能超过0B8000h，那是缓存显示的地方
BeginSectorNo           equ     1               ; 从软盘的第1个扇区开始读，软盘的第0个扇区是引导扇区
SegmentBaseOfSector     equ     01000h          ; 从0x1000:0x0000处开始存放读取的数据
BytesPerSector          equ     0200h           ; 每扇区的字节数=512字节
CyclesNum               equ     80              ; 软盘共80个柱面
SectorsPerTrack         equ     18              ; 每个磁道的扇区数量
SegMaxOffset            equ     010000h         ; 每个段的最大长度为64K

    org 07c00h
    mov ax, cs
    mov ds, ax
    mov es, ax
    mov ss, ax
    mov sp, 0400h

    call ClearScreen

    mov ax, StartRead       ; 待显示字符串
    mov cx, StartReadLen    ; 待显示字符串长度
    call DispStr

    call ReadFloppy

    jmp $


_display_position   dw  0                   ; 默认位置是0行0列，屏幕总大小是25行80列
ReadError:          db  "Read error!!!"
ReadErrorLen        equ $ - ReadError
Dot                 db "."
DotLen              equ $ - Dot
StartRead           db "Reading image."
StartReadLen        equ $ - StartRead
FinishRead          db "[100%] finished ^_^"
FinishReadLen       equ $ - FinishRead

;=====================================================================
; 使用BIOS提供的int 0x13中断读扇区，加载system模块
; 参数说明：
;   ah = 0x02 表示读磁盘扇区；al = 待读取的扇区数量
;   ch = 磁道（柱面）号的低8位；cl = 开始扇区(位0-5)，磁道号高2位（位6-7）
;   dh = 磁头号；dl = 驱动器号
;   es:bx 指向数据缓冲区，如果出错那么CF标志位置位
; 返回值：
;   成功则ah=0，al存放读取的扇区数量；失败则ah=出错码
;
; 软盘共80个柱面（0-79），每个柱面有2个磁道（磁道0-对应磁头0，磁道1-对应磁头1），每个磁道有18个扇区，每个扇区512字节
; 每次读取一个扇区，并计算柱面，磁道号，磁头号，该磁道的起始扇区
; 计算方法：
;   驱动器号 = 0， 表示第1个软盘
;   Q = 软盘当前待读取的扇区号 / 18
;   R = 软盘当前待读取的扇区号 % 18
;   柱面号 = Q / 2
;   磁头号 = Q & 1      0 或 1
;   当前磁道的起始扇区 = R + 1

_read_bytes_for_seg     dd      0               ; 当前段已读取的字节数量
_current_sector_no      dw      BeginSectorNo   ; 当前读取的扇区号
_current_track_head_no  db      0               ; 当前磁头号


ReadFloppy:
    push es
    push bx

    mov ax, SegmentBaseOfSector
    mov es, ax
    mov bx, 0                       ; 初始化指向 01000:0x0000位置
.loop_read:
    mov ax, [_current_sector_no]
    ; 每读取64个扇区，打印一个"."
    and ax, 003Fh
    cmp ax, 0
    jne .read_1

    push es
    push bx             ; DispStr也会改变bx寄存器
    mov ax, ds
    mov es, ax
    mov ax, Dot       ; 待显示字符串
    mov cx, DotLen    ; 待显示字符串长度
    call DispStr
    pop bx
    pop es
    
.read_1:
    ; 中断调用的入参计算
    xor ax, ax
    mov ax, [_current_sector_no]
    mov cl, SectorsPerTrack
    div cl                          ; Q - al，R - ah
    
    mov dh, al
    and dh, 01h     ; 磁头号 = Q & 1
    mov dl, 0       ; 驱动器号为0，表示第一个软盘

    mov ch, al
    shr ch, 1       ; 柱面号 = Q / 2

    mov cl, ah
    inc cl          ; 当前磁道的起始扇区 = R + 1

    mov ax, 0201h   ; ah - 0x2 表示读扇区，al - 0x1 表示每次读取1个扇区
    
    int 013h
    jc .read_error  ; 读取出错

    mov eax, [_read_bytes_for_seg]
    add eax, BytesPerSector
    cmp eax, SegMaxOffset

    jae .move_to_new_seg        ; 当前段的64K已经存满，需要从下一个段开始存

    ; 继续往当前段存储
    add bx, BytesPerSector
    mov dword [_read_bytes_for_seg], eax
    jmp .if_sector_num_reached

.move_to_new_seg:
    mov ax, es
    add ax, SegmentBaseOfSector
    mov es, ax
    mov bx, 0
    mov dword [_read_bytes_for_seg], 0

.if_sector_num_reached:
    mov ax, [_current_sector_no]
    inc ax                              ; 判断是否已经读取足够的扇区数量
    mov word [_current_sector_no], ax
    cmp ax, LastSectorNoToRead
    ja .finish_read                     ; 已经读完，则退出
    jmp .loop_read                      ; 继续读取

.read_error:
    push es
    push bx
    mov ax, ds
    mov es, ax
    mov ax, ReadError       ; 待显示字符串
    mov cx, ReadErrorLen    ; 待显示字符串长度
    call DispStr
    pop bx
    pop es

.finish_read:
    push es
    push bx
    mov ax, ds
    mov es, ax
    mov ax, FinishRead       ; 待显示字符串
    mov cx, FinishReadLen    ; 待显示字符串长度
    call DispStr
    pop bx
    pop es

    pop bx
    pop es
    ret


;=====================================================================
; DispStr：使用BIOS提供的中断，来实现字符串显示操作
; 前提：假设带显示的字符串已经位于ax寄存器中，cx中存放待显示字符的长度，dx存放起始坐标
; 参数介绍
; ah = 13h，功能码，显示字符串
; al = 显示方式。 1）0表示字符串中只包含字符，显示属性在bl中，光标位置不变；
;               2）1表示字符串中只包含字符，显示属性在bl中，光标位置改变；
;               3）2表示字符串中包含显示字符和属性，显示后，光标位置不变；
;               4）3表示字符串中包含显示字符和属性，显示后，光标位置改变；
; bh = 页码
; bl = 属性
; cx = 待显示字符串的长度
; (dh, dl) = 显示起始坐标(行, 列)
; es:bp = 显示字符串的位置
DispStr:
    mov bp, ax          ; 假设调用前显示字符串的地址已经存放在寄存器ax中
                        ; 假设待显示字符个数存放在cx寄存器中，因此此处不需要设置
    mov ah, 13h         ; 功能码13h
    mov al, 1           ; 显示方式为1：字符串中只包含字符，显示属性在bl中，光标位置改变

    mov bl, 0ch         ; 显示属性：黑底红字
    mov bh, 0           ; 页码 0，当前还没有滚屏操作

    mov dx, [_display_position]
    add dl, cl
    cmp dl, 80
    jae .new_line       ; >= 80
    sub dl, cl
    jmp .show
.new_line:
    sub dl, cl
    push cx
    xor cx, cx
    mov cl, 80
    sub cl, dl
    int 10h
    add bp, cx
    pop cx
    add cl, dl
    sub cl, 80
    inc dh
    mov dl, 0
.show:
    int 10h

    add dl, cl
    cmp dl, 80
    jae .next_line
    jmp .finish_disp
.next_line:
    mov dl, 0
    inc dh
.finish_disp:
    mov word [_display_position], dx
    ret
; DispStr 结束

;=====================================================================
; ClearScreen：清屏操作，使用BIOS提供的10h中断实现
; 前置条件：无
; 参数介绍：
; ah = 0x06，向上滚屏；ah = 0x07h，向下滚屏
; al = 滚动行数（0表示清窗口）
; bh = 空白区域的缺省属性
; (ch, cl) = 窗口的左上角位置(Y坐标，X坐标)
; (dh, dl) = 窗口的右下角位置(Y坐标，X坐标)
ClearScreen:
    mov ah, 06h
    mov al, 0       ; 清窗口
    mov cl, 0       ; 待清理窗口左上角的位置
    mov ch, 0
    mov dl, 79      ; 待清理窗口右下角的位置，窗口的大小80列 *25行
    mov dh, 24
    mov bh, 07h     ; 黑底白字
    int 10h
    ret
; ClearScreen 结束

times 510-($-$$)    db 0
dw 0xaa55